feat: read the engine's unfulfilled-keys report instead of diffing declared vs delivered - #1308
ralphstodomingo wants to merge 10 commits into
Conversation
…clared vs delivered The MCP catalog now keeps the `_meta` of a server's last tools/list page per client, exposed as `MCP.listMeta(name)`. On attach, the gaps come from the engine's `ai.altimate/unfulfilled` report, grouped by reason in the toast and headless line with the engine's detail (e.g. `spawn docker ENOENT`); `no-bridge` entries stay out of the missing set as before. The attached outcome carries the full report. `MIN_ENGINE_VERSION` moves to 0.7.2, the first engine that emits it; an engine that sends none claims no gaps rather than inventing them. Closes #1307 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Env-guarded (`ALTIMATE_ENGINE_E2E_ROOT`), skipped otherwise: spawns a built engine over stdio the way the overlay does, against a fake Altimate API and a real second MCP server, and reads the `ai.altimate/unfulfilled` report through `MCP.listMeta` into the attach toast text. The engine is a node shebang script, so the test spawns node rather than the bun test runner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Marker Guard flagged the changed lines in the upstream-shared catalog; the single-line marker comments did not count as a wrapped block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Engine → altimate-code, through the real attach pathThis runs the CLI's production attach code under a real instance with no model turn: the binding cache, the Declared by the workspace: Result — release candidate (head
|
Custom (tenant-created) integrations carry numeric ids; the parser treated the whole report as malformed over that one field and the attach announced no gaps at all. Take the id as a string. Found by the engine-to-CLI run against a local backend with a custom MCP integration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
One more commit on this head, |
Multi-model review — client halfReviewed jointly with AltimateAI/altimate-mcp-engine#248 as one feature. The wire contract between the two halves agrees exactly: meta key, all four field names, all six reason spellings, and the empty-array-vs-absent distinction match. Major1. Tools and their report are not committed atomically across a refresh
Consequences:
This bites C3 ("the catalog keeps the report across the paths that list tools") on the Suggested: accumulate Minor2. The headline and the gap line can disagree (raw vs sanitized key space)
So a served The undercount itself pre-dates this PR — the old Suggested: normalize both sides to one key space before the 3.
Suggested: group by 4. "Last page's
Suggested: pick a rule (last-page-authoritative vs any-page), implement it inside the completed snapshot, and add a first-page-only case. 5. Truncation is not redaction
6.
Verified sound
|
…hat a gap is with its own detail Answers the multi-model review of the unfulfilled report, client half. - the tools of a listing and its _meta are committed in one statement (State.meta beside State.defs) and read through one accessor, MCP.snapshot(name): a refresh that is pending or that failed leaves the last good pair standing, and the overlay can no longer pair one listing's tools with another's report - the catalog commits _meta when a listing completes — the last page that carries one wins, a listing with none clears it — instead of clearing at the start - served counts compare the declared keys in the catalog's sanitised key space, so the headline cannot undercount a served tool whose raw key the MCP layer renamed - the missing line groups by reason AND integration, so one integration's error is never printed as another's - spawn-failed reads 'server could not be started or reached', which is what the engine records under it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Re-review disposition —
|
|
@codex review Scoped review against the claims below (head Claims
Residuals (already accepted)
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45a8d02c8c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (typeof key !== "string" || typeof id !== "string" || typeof reason !== "string") return undefined | ||
| out.push({ key, integrationId: id, reason, ...(typeof detail === "string" && detail !== "" ? { detail } : {}) }) |
There was a problem hiding this comment.
Reject malformed detail fields
C1 does not hold when an otherwise valid entry has a non-string detail, such as { key: "x", integrationId: "i", reason: "exception", detail: 42 }. This branch silently discards the invalid field and accepts the report, so the outcome receives both missing and unfulfilled and may announce a gap instead of failing closed with undefined.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 820147a: a present detail that is not a string fails the whole report, like the other fields. Test: "an entry with a malformed detail is a malformed report, not a report missing a field" — number, null and object details give undefined; a string, an empty string and an absent field parse.
| s.defs[name] = listed | ||
| s.meta[name] = McpCatalog.listMeta(client) |
There was a problem hiding this comment.
Associate metadata directly with each refresh result
C3 does not hold when two ToolListChanged notifications start overlapping listings for the same client. If listing A completes and then listing B updates listMetaByClient before A's awaiting handler resumes, this lookup returns B's metadata while listed still contains A's tools; the state then commits a mismatched pair and snapshot() can produce a false or omitted gap. The listing operation needs to return its own metadata alongside its definitions rather than rereading a client-wide cache.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 820147a, as suggested: McpCatalog.defsWithMeta returns the listing and its _meta as one value, and every commit — create, the tools/list_changed handler, storeClient on connect and after OAuth — stores that pair rather than re-reading the per-client value. Test: "defsWithMeta hands back the listing and its own _meta as one value". listMeta(client) remains for the catalog tests and is no longer on the commit path.
| // layer sanitised them (`[a-zA-Z0-9_-]`), while the declaration carries the | ||
| // raw keys, so a raw key with any other character would never count as served | ||
| // and the headline would disagree with a report that names no gap. (multi-model review) | ||
| const served = declared ? declared.keys.filter((k) => present.has(sanitize(k))).length : present.size |
There was a problem hiding this comment.
Avoid overcounting colliding sanitized keys
C5 does not hold when distinct raw declarations sanitize to the same catalog key. For example, with declared keys foo.bar and foo_bar, a listing containing only foo_bar, and an unknown-key report for foo.bar, both declarations pass this predicate because each sanitizes to foo_bar; the headline says 2 of 2 available while the gap line says foo.bar is unavailable. The same collision-prone test is also used for extension keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 820147a: served and extServed also exclude any key the engine reports unfulfilled, so of two raw keys that sanitise to one catalog name only the one the report does not name counts. Test: "the headline counts in the catalog's key space, and never a key the report names" — foo.bar + foo_bar over a served datamate_foo_bar with foo.bar reported reads "1 of 2".
…er counts a reported key Answers the Codex round on the report fixes: - McpCatalog.defsWithMeta returns the listing and its _meta as one value, and every commit of a listing stores that pair — not a per-client value another refresh may have overwritten while this one was awaiting - served counts exclude keys the engine reports unfulfilled, so two raw keys that sanitise to one catalog name cannot both count as served - parseUnfulfilled rejects an entry whose detail is present but not a string, failing closed like the other fields Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
@codex review Scoped falsification round on the fixes since the last round ( Fix claims
Accepted residuals
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 820147aee2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const served = declared | ||
| ? declared.keys.filter((k) => present.has(sanitize(k)) && !reported.has(k)).length |
There was a problem hiding this comment.
Count each sanitized catalog entry at most once
When both colliding raw tools are returned and the engine therefore reports neither as unfulfilled—for example, declarations and listings containing both foo.bar and foo_bar with an empty report—both predicates succeed even though the MCP catalog sanitizes both names to the single foo_bar entry and one overwrites the other. The headline consequently says 2 of 2 available while only one catalog tool is callable, so the counting needs to consume or deduplicate sanitized catalog keys rather than independently counting every matching declaration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right — with both listed and neither reported, the count was still per declaration. Fixed in 9452109: served and extServed are the number of DISTINCT sanitised catalog entries that are present and unreported, so foo.bar + foo_bar over one datamate_foo_bar count once. Test: "two declarations that sanitise to one catalog entry count once, even with nothing reported" reads "1 of 2".
Two raw keys that sanitise to one catalog name are one callable tool however many the engine lists; served and extension counts are the number of distinct sanitised entries that are present and unreported. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
|
@codex review Final scoped falsification round (round 3 of 3) on the fix since the last round ( Fix claim
Accepted residuals
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9452109654
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // live IDE bridge; when they do they are real capability and the line names | ||
| // them, but their absence is the normal no-IDE case, never `missing`. | ||
| const extServed = declared ? declared.extensionKeys.filter((k) => present.has(k)).length : 0 | ||
| const extServed = declared ? servedEntries(declared.extensionKeys) : 0 |
There was a problem hiding this comment.
Deduplicate catalog entries across declaration groups
G1 still fails when an ordinary declaration and an extension declaration collide after sanitization. For example, with keys: ["foo.bar"], extensionKeys: ["foo_bar"], one catalog entry datamate_foo_bar, and an empty unfulfilled report, these separate calls each return 1, so the toast reports one integration tool plus one extension tool even though the catalog contains only one callable entry. Track consumed sanitized entries across both counts so a collision is counted only once.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 413fadc: sanitised entries are consumed across both groups, so an ordinary key and an extension key that collide count once, with the ordinary keys. Test: "a collision across the ordinary and extension groups is one entry, counted once" reads "1 of 1" with no extension line.
…nd extension groups Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Disposition after three Codex rounds —
|
…kers Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
…w as bare Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Issue for this PR
Closes #1307
Type of change
What does this PR do?
When a workspace is bound, the attach toast says "N of M declared integration tools available" and lists what is declared but absent. Until now that list came from a client-side diff: fetch the workspace's allowlist from the API, subtract the tool names the engine served. A diff can name keys, never reasons — an expired Jira token, an MCP server whose binary is not installed, an integration the tenant removed from the catalog, an extension tool with no VS Code window, and a key the provider does not offer all read the same.
@altimateai/datamate0.7.2 (AltimateAI/altimate-mcp-engine#248) reports every declared-but-unserved key with a reason under_meta["ai.altimate/unfulfilled"]on eachtools/listresponse. This PR reads it:_metaof a server's lasttools/listpage per client.paginatekeeps only each page's items, so the result object — the only carrier of_meta— was dropped. A listing starts with none; any page that carries one sets it; a listing without one clears it. Exposed asMCP.listMeta(name)(undefined while not connected).no-bridgeentries stay out of the "missing" line, as absent extension tools without an IDE were already treated as expected; every other reason is named, grouped, with the engine's detail (e.g.spawn docker ENOENT) —Declared but not available — no usable connection: jira_search_issues; server failed to start (spawn docker ENOENT): gh_list_prs, gh_create_pr.The "N of M" headline still counts declared keys that are present. The attached outcome carries the full report for later surfaces._meta(nothing at or above the floor does) yields an outcome with neithermissingnorunfulfilled, and a toast with no gap line — not "all served".MIN_ENGINE_VERSION), the first engine that emits the report. Do not merge before@altimateai/datamate@0.7.2is on npm (AltimateAI/altimate-mcp-engine#249 is the bump); until then every attach would refuse with "needs 0.7.2 or newer".The client no longer reads what it cannot know: the allowlist lookup (
declared()) is kept only for the headline's denominator and the extension-tool count, and a report without a reachable allowlist still names the gaps.Claims
missingandunfulfilledexist on the outcome only when the engine sent a well-formed report; a malformed or absent_metayields neither (test: "an engine that sends no report is not read as having no gaps";parseUnfulfilledcases).no-bridgenever counts as missing, and every other reason does — includingunknown-keyon an extension key while a bridge is connected (reportedMissing; test "no-bridge entries in the report are expected, never missing").tools/list_changedrefresh, the post-OAuth reconnect all go throughMcpCatalog.defs→listTools, which is the only writer (catalog-list-meta.test.tscovers first page, multi-page, and clearing).key=reason; test "a gap whose reason changed is announced again")._metais retained per client but read only fordatamate; tool conversion and the storeddefsare unchanged.Residuals
MIN_ENGINE_VERSIONcomment.How did you verify your code works?
bun run typecheckclean; prettier clean on the files this PR touches (the files that were already non-conforming onmainare left as they were).test/altimate/workspace(all),test/mcp/catalog-list-meta.test.ts,test/altimate/precedence-guard-order.test.ts: 460 pass.test/mcpand the two session suites whose MCP stubs gainedlistMeta: 279 pass; the 5mcp.headersfailures and 1oauth-auto-connectfailure reproduce identically on an untouchedmaincheckout (environmental, not this change).New tests: 6 attach cases (reasons in the toast, no-bridge exclusion, no-report, report-without-allowlist, reason-change re-announce, the existing inventory case now stating the engine's report),
describeMissing/parseUnfulfilled/reportedMissingunit cases, 3 catalog cases over a real in-memory MCP server.End to end through the real MCP service (
test/mcp/engine-unfulfilled.e2e.test.ts, env-guarded, skipped in CI): the engine atAltimateAI/altimate-mcp-engine#248's head built as 0.7.2 is spawned over stdio byMCP.addexactly as the overlay spawns it, against a fake Altimate API, a real second MCP server and a missing binary;MCP.listMeta("datamate")returns the five-entry report with the expected reasons and the toast text readsDeclared but not available — no usable connection: jira_search_issues; not offered by the integration: ghost; server failed to start (spawn altimate-e2e-missing-binary ENOENT): whatever; no longer in the catalog: retired_tool.— 1 pass. Run it withALTIMATE_ENGINE_E2E_ROOT=<engine checkout with dist/> bun test test/mcp/engine-unfulfilled.e2e.test.tsfrompackages/opencode.Engine → CLI through the real attach path (evidence):
bootstrap+beforeTurnon a bound directory against the 0.7.2 release candidate (engine PRs 250 + 248 merged, built locally, on PATH asdatamate). Settled outcomeattachedwithdeclared: 5,missing: [jira_search_issues, ghost, whatever, retired_tool], the full report incl. theno-bridgeentry, and the exact toast text; 8/8 checks. A 0.7.1 build is refused asengine-too-oldwith the install line; 2/2.Screenshots / recordings
Not a UI change beyond toast text; the exact strings are asserted in the tests above.
Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01GHBUvb843k1R7UAGi8Ya9b
Appendix — complexity delta (altimate-code: engine unfulfilled report)
e8c21c2af7→93c879af8· only functions this diff touches · advisory, not a gate.✅ No touched function changed in complexity (12 touched, 4 new, all under 10).
ℹ️ How to read these numbers
Cognitive (Sonar spec) counts breaks in linear reading flow — each
if/loop/catch/ternary/boolean-operator switch adds 1, and nesting makes every further break cost more. It approximates how much you must hold in your head to follow the function: 0–5 trivial · 6–10 easy · 11–15 moderate (15 = Sonar's recommended per-function cap) · 16–25 hard to follow · >25 needs decomposition.CCN (cyclomatic) counts independent paths — also the minimum number of test cases for full branch coverage of the function.
Only functions this diff touches are measured, as deltas — pre-existing complexity is not counted against this change. Rising numbers aren't automatically wrong; they're where review attention should go. Test files excluded.
Summary by cubic
Closes #1307. Workspace attach now reads the engine’s
ai.altimate/unfulfilledreport instead of diffing declared and delivered tools, so missing-tool notices include actionable reasons and details rather than only tool names.Attach behavior
no-bridgeentries; every other valid reason is reported.missingandunfulfilledunset when the report is absent or malformed.MCP catalog
tools/list_metaand exposes it throughMCP.listMeta(name).MCP.snapshot(name)to prevent mismatched refreshes._metaclears it.MIN_ENGINE_VERSIONto 0.7.2, which requires@altimateai/datamate0.7.2 or newer.Written for commit ef0b8ed. Summary will update on new commits.